Day 27 我們完成了 Product API,但目前 ProductsController 同時負責:
當專案越來越大,Controller 容易累積太多程式邏輯。
因此,我們希望把責任分開:
ProductsController
→ 處理 HTTP Request / Response
ProductService
→ 處理 Product 相關操作
但這時會出現一個問題:
ProductsController 需要 ProductService,那 ProductService 應該由誰建立?
最直覺的方式可能是:
private readonly ProductService _productService =
new ProductService();
但這代表 Controller 不只要「使用」Service,還要負責「建立」Service。
ASP.NET Core 提供了另一種方式:
Dependency Injection
DI
依賴注入
今天就從目前的 Product API 出發,理解 DI 如何解決這個問題。
假設:
ProductsController
↓
需要 ProductService
ProductService 就是 ProductsController 的:
Dependency
→ 依賴
如果直接:
ProductService productService =
new ProductService();
代表 Class 自己負責建立 Dependency。
而 Dependency Injection 的做法是:
Class 不自己建立需要的 Dependency,而是由外部提供。
兩種方式可以這樣比較:
自己 new |
Dependency Injection |
|---|---|
| Class 自己建立 Dependency | Dependency 由外部提供 |
| 使用與建立綁在一起 | 使用與建立分開 |
| Dependency 較難替換 | Dependency 較容易替換 |
| 自己管理生命週期 | 可交由 DI Container 管理 |
所以今天先建立一個最重要的概念:
自己 new
→ 自己建立、自己使用
DI
→ 外部建立、Class 負責使用
ASP.NET Core 本身就有內建的 DI 機制,可以幫我們管理這些 Service。
今天要把原本 Controller 裡的 Product 操作抽到:
ProductService
專案結構調整成:
MyApi2
│
├─ Controllers
│ └─ ProductsController.cs
│
├─ Models
│ ├─ Product.cs
│ ├─ CreateProductRequest.cs
│ └─ UpdateProductRequest.cs
│
├─ Services
│ ├─ IProductService.cs
│ └─ ProductService.cs
│
└─ Program.cs
各自負責:
| 檔案 | 負責什麼? |
|---|---|
IProductService.cs |
定義 Product Service 的功能 |
ProductService.cs |
實作 Product 相關操作 |
ProductsController.cs |
處理 HTTP Request / Response |
Program.cs |
註冊 Service |
Models 繼續沿用 Day 27,不需要重新建立。
IProductService.cs建立:
Services/IProductService.cs
完整程式碼:
using MyApi2.Models;
namespace MyApi2.Services;
public interface IProductService
{
IEnumerable<Product> GetAll();
Product? GetById(int id);
Product Create(CreateProductRequest request);
bool Update(
int id,
UpdateProductRequest request
);
bool Delete(int id);
}
IProductService 是 Interface。
它負責定義:
Product Service 可以提供哪些操作。
但不負責真正處理資料。
可以先理解成:
IProductService
→ 定義要做什麼
ProductService
→ 負責實際怎麼做
補充一點:
DI 並不一定需要搭配 Interface。
今天使用 IProductService,是讓 Controller 依賴抽象,也方便之後替換實作與測試。
ProductService.cs建立:
Services/ProductService.cs
完整程式碼:
using MyApi2.Models;
namespace MyApi2.Services;
public class ProductService : IProductService
{
private static readonly List<Product> Products =
[
new Product(1, "Mouse", 1000m)
];
private static int nextId = 2;
public IEnumerable<Product> GetAll()
{
return Products;
}
public Product? GetById(int id)
{
return Products.FirstOrDefault(
product => product.Id == id
);
}
public Product Create(
CreateProductRequest request
)
{
Product product = new(
nextId,
request.Name,
request.Price
);
nextId++;
Products.Add(product);
return product;
}
public bool Update(
int id,
UpdateProductRequest request
)
{
int index = Products.FindIndex(
product => product.Id == id
);
if (index == -1)
{
return false;
}
Products[index] = new Product(
id,
request.Name,
request.Price
);
return true;
}
public bool Delete(int id)
{
Product? product =
Products.FirstOrDefault(
product => product.Id == id
);
if (product is null)
{
return false;
}
Products.Remove(product);
return true;
}
}
現在原本 Controller 裡的:
Products
nextId
查詢
新增
修改
刪除
都移到了 ProductService。
責任開始分開:
ProductsController
→ HTTP Request / Response
ProductService
→ Product 操作
目前的:
static List<Product>
仍然只是模擬 Database。
之後學習 EF Core 時,這裡會改成真正的資料庫操作。
Program.cs:註冊 Service現在已經有:
IProductService
ProductService
接下來還要告訴 ASP.NET Core:
當程式需要
IProductService時,要提供ProductService。
修改:
Program.cs
完整程式碼:
using MyApi2.Services;
var builder =
WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<
IProductService,
ProductService
>();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
今天最重要的是:
builder.Services.AddScoped<
IProductService,
ProductService
>();
可以先直接讀成:
需要 IProductService
↓
提供 ProductService
其中:
builder.Services
→ 註冊 Service
IProductService
→ 要使用的 Service Type
ProductService
→ 實際提供的實作
AddScoped
→ 設定 Service Lifetime
這一步稱為:
註冊 Service
ProductsController.cs接著修改:
Controllers/ProductsController.cs
完整程式碼:
using Microsoft.AspNetCore.Mvc;
using MyApi2.Models;
using MyApi2.Services;
namespace MyApi2.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(
IProductService productService
)
{
_productService = productService;
}
[HttpGet]
public ActionResult<IEnumerable<Product>>
GetAll()
{
IEnumerable<Product> products =
_productService.GetAll();
return Ok(products);
}
[HttpGet("{id:int}")]
public ActionResult<Product> GetById(int id)
{
Product? product =
_productService.GetById(id);
if (product is null)
{
return NotFound();
}
return Ok(product);
}
[HttpPost]
public ActionResult<Product> Create(
CreateProductRequest request
)
{
Product product =
_productService.Create(request);
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
product
);
}
[HttpPut("{id:int}")]
public IActionResult Update(
int id,
UpdateProductRequest request
)
{
bool updated =
_productService.Update(
id,
request
);
if (!updated)
{
return NotFound();
}
return NoContent();
}
[HttpDelete("{id:int}")]
public IActionResult Delete(int id)
{
bool deleted =
_productService.Delete(id);
if (!deleted)
{
return NotFound();
}
return NoContent();
}
}
跟 Day 27 比較,Controller 已經不再直接:
操作 List<Product>
產生 nextId
新增 Product
修改 Product
刪除 Product
而是改成呼叫:
_productService.GetAll();
_productService.GetById(id);
_productService.Create(request);
_productService.Update(id, request);
_productService.Delete(id);
Controller 現在比較專注在:
接收 HTTP Request
↓
呼叫 Service
↓
根據結果回傳 HTTP Response
例如:
Product? product =
_productService.GetById(id);
if (product is null)
{
return NotFound();
}
return Ok(product);
Controller 負責決定:
找到 Product
→ 200 OK
找不到 Product
→ 404 Not Found
至於 Product 實際怎麼取得,則交給 ProductService。
現在看整篇最重要的一段:
private readonly IProductService _productService;
public ProductsController(
IProductService productService
)
{
_productService = productService;
}
這種方式稱為:
Constructor Injection
→ 建構子注入
Constructor:
public ProductsController(
IProductService productService
)
可以直接理解成:
建立 ProductsController 時,我需要一個
IProductService。
因為前面已經在 Program.cs 建立:
IProductService
→ ProductService
的註冊關係,所以 ASP.NET Core 可以找到對應的 ProductService,並提供給 Controller。
Controller 再把收到的 Dependency 保存:
_productService = productService;
之後 Action 就可以直接使用:
_productService.GetById(id);
整個過程:
Controller
宣告需要 IProductService
↓
DI Container
找到 ProductService
↓
提供給 Constructor
↓
Controller 使用 Service
Controller 不需要自己:
new ProductService();
前面註冊 Service 時使用:
builder.Services.AddScoped<
IProductService,
ProductService
>();
其中:
Scoped
代表 Service 的:
Lifetime
→ 生命週期
ASP.NET Core 常見三種:
| Lifetime | 基本概念 |
|---|---|
Transient |
每次向 Container 解析時建立新的 Instance |
Scoped |
同一個 Request Scope 共用相同 Instance |
Singleton |
Application 共用相同 Instance |
今天使用:
AddScoped
在一般 Web Request 中,可以先理解成:
Request A
→ 一個 ProductService Instance
Request B
→ 另一個 ProductService Instance
同一個 Request Scope 中取得同一個 Scoped Service,會使用相同的 Instance。
目前先理解三種 Lifetime 的基本差異即可,不需要深入所有使用情境。
後面學習 EF Core 時,還會再次看到 Scoped。
現在回到最初的問題:
ProductsController 需要 ProductService,那 ProductService 到底從哪裡來?
其實只需要看三個位置。
Program.cs
builder.Services.AddScoped<
IProductService,
ProductService
>();
建立:
IProductService
→ ProductService
的註冊關係。
ProductsController.cs
public ProductsController(
IProductService productService
)
{
_productService = productService;
}
Controller 宣告需要:
IProductService
由 DI Container 提供。
Controller Action:
Product? product =
_productService.GetById(id);
直接使用已經注入的 Service。
所以整個 DI 流程可以濃縮成:
Program.cs
註冊
IProductService → ProductService
↓
ProductsController
Constructor Injection
↓
_productService
↓
Controller Action
使用 Service
也就是今天最重要的三個步驟:
註冊 → 注入 → 使用
Day 27 的 Controller:
ProductsController
→ HTTP
→ Product 操作
Day 28 把責任拆開:
ProductsController
→ HTTP Request / Response
ProductService
→ Product 操作
再透過 DI 把兩者連接:
Program.cs
→ 註冊
Constructor
→ 注入
Controller Action
→ 使用
Dependency Injection 的核心,是讓 Class 專心使用 Dependency,把 Dependency 的建立與管理交給外部。